[Terraform] EC2 Image Builderでゴールデンイメージを配布し、別AWSアカウントの起動テンプレートに登録する
どうも、ちゃだいん(@chazuke4649)です。
ゴールデンイメージ管理やってますか?
ゴールデンイメージ管理の課題として、組織が大きくなりAWSアカウントが増えると、ゴールデンイメージ管理は煩雑になっていきます。
EC2 Image Builder は、OSイメージであるAMIのビルドから配布まで集中管理・自動化を実現してくれるサービスです。AMIの配布については、RAMによりOrganizations連携ができるため、マルチアカウント環境下におけるAMI管理を1つのアカウントで集中管理・配布することが可能です。
EC2 Image BuilderのアップデートでAMIの配布から拡張し、同じアカウントなら起動テンプレート自体の更新まで行ってくれるようになりました。ここまでやってくれると、あとは EC2 AutoScaling でインスタンス更新をすれば、latestバージョンの起動テンプレートを使用することによる、新しいAMIへの差し替えが容易に行えます。
ただし、本機能はまだマルチアカウントでは未サポートです。
今回は、ここの部分をTerraformによってもう少しやりやすくしてみようと思います。
具体的には、配布先のアカウントで terraform plan
すれば、新しく配布されたAMI IDを検知し、起動テンプレートのバージョン更新を change
として上げてくれる状態にすることです。
やってみる
前提
- AWS Organizations環境下に2つのアカウントが存在する
- 管理アカウント
000000000000
- Sharedアカウント
111122223333
: AMI配布元 - Workloadアカウント
444455556666
: AMI配布先
手順
- Image Builderを構築する[Sharedアカウント作業]
- 起動テンプレートを構築する[Workloadアカウント作業]
- Image Builderにより新しいAMIを作成・配布する[Sharedアカウント作業]
- 起動テンプレートを更新する[Workloadアカウント作業]
1. Image Builderを構築する[Sharedアカウント作業]
% tree . ├── backend.tf ├── files │ └── build.yml ├── iam-role.tf ├── image-builder.tf └── sg.tf
メインとなるimage-builder.tfは以下の通りです。
image-builder.tf
data "aws_partition" "current" {} data "aws_region" "current" {} ## Image Pipeline resource "aws_imagebuilder_image_pipeline" "sample" { name = "sample" image_recipe_arn = aws_imagebuilder_image_recipe.sample.arn infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.sample.arn distribution_configuration_arn = aws_imagebuilder_distribution_configuration.sample.arn } ## Image recipe resource "aws_imagebuilder_image_recipe" "sample" { name = "sample" parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" version = "1.0.0" block_device_mapping { device_name = "/dev/xvdb" ebs { delete_on_termination = true volume_size = 100 volume_type = "gp3" } } component { component_arn = aws_imagebuilder_component.build.arn } } ## Builder component "Build" resource "aws_imagebuilder_component" "build" { name = "build" platform = "Linux" version = "1.0.0" data = file( "./files/build.yml" ) } ## Infrastructure configration resource "aws_imagebuilder_infrastructure_configuration" "sample" { name = "sample" description = "this is sample" instance_profile_name = aws_iam_instance_profile.image_builder.name instance_types = ["t3.small"] security_group_ids = [aws_security_group.test_sg.id] subnet_id = "subnet-xxxxxxxxxxxxxxxxxxxx" terminate_instance_on_failure = true } ## Distribution configration resource "aws_imagebuilder_distribution_configuration" "sample" { name = "sample" distribution { ami_distribution_configuration { name = "sample_v{{imagebuilder:buildVersion}}_{{imagebuilder:buildDate}}" ami_tags = { ImageBuilder = true } launch_permission { organization_arns = ["arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx"] } } region = data.aws_region.current.id } }
いくつか補足します。
Builder component "Build"
Builder componentは、YAMLファイルを外出しています。
## Builder component "Build" resource "aws_imagebuilder_component" "build" { name = "build" platform = "Linux" version = "1.0.0" data = file( "./files/build.yml" ) }
build.ymlはこちらから拝借し「jq」コマンドを Amazon Linux2のリポジトリ、 新しいLinuxカーネルを拡張リポジトリからインストールしています。
name: yum_install description: jq_install schemaVersion: 1.0 phases: - name: build steps: - name: UpdateOS action: UpdateOS - name: yum_update action: ExecuteBash inputs: commands: - yum update -y - name: jq_install action: ExecuteBash inputs: commands: - yum install jq -y - name: kernel_ng_install action: ExecuteBash inputs: commands: - amazon-linux-extras install -y kernel-ng - name: validate steps: - name: jq_install action: ExecuteBash inputs: commands: - rpm -qi jq
Infrastructure configration
インフラ設定では、配布するAMIの元となるEC2を実際に起動するVPCサブネットやセキュリティグループを指定します。今回サブネットはパブリックサブネットにし、後述するセキュリティグループでアウトバウンド通信のみ許可しています。(VPCサブネットはIDを直接指定しているため、コードを利用する際は適宜置き換えが必要です)
## Infrastructure configration resource "aws_imagebuilder_infrastructure_configuration" "sample" { name = "sample" description = "this is sample" instance_profile_name = aws_iam_instance_profile.image_builder.name instance_types = ["t3.small"] security_group_ids = [aws_security_group.test_sg.id] subnet_id = "subnet-xxxxxxxxxxxxxxxxxxxx" terminate_instance_on_failure = true }
Distribution configration
配布設定では、配布するAMI名(※Nameタグではない)を指定できます。以下サンプルの場合例えば sample_v4_2022-08-10T09-01-50.490Z
のように指定できます。
{{imagebuilder:buildVersion}}
は同じイメージ名のビルドバージョンであり、Image recipeで指定するバージョンとは異なる点に注意です。
また、Launch permissionにて共有先組織を指定しており、今回は組織全体へ共有しています。
## Distribution configration resource "aws_imagebuilder_distribution_configuration" "sample" { name = "sample" distribution { ami_distribution_configuration { name = "sample_v{{imagebuilder:buildVersion}}_{{imagebuilder:buildDate}}" ami_tags = { ImageBuilder = true } launch_permission { organization_arns = ["arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx"] } } region = data.aws_region.current.id } }
iam-role.tf
IAMロールは以下を参考に作成します。
Prerequisites - EC2 Image Builder
resource "aws_iam_instance_profile" "image_builder" { name = "image_builder" role = aws_iam_role.image_builder.name } resource "aws_iam_role" "image_builder" { name = "ImageBuilderRole" path = "/" managed_policy_arns = [ "arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder", "arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds", "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" ] assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "ec2.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] } EOF }
sg.tf
セキュリティグループは、以下ブログと同じパターンで、アウトバウンド通信のみ許可します。
Terraformのfor_eachとnullで、効率的にAWSのセキュリティグループを定義する | DevelopersIO
## Security Groups locals { test_sg = { ## [ type, from_port, to_port, protocol, sg-id, cidr_blocks, description ] "rule_3" = ["egress", 0, 0, "-1", null, ["0.0.0.0/0"], "Allow any outbound traffic"] } } resource "aws_security_group" "test_sg" { vpc_id = module.vpc.vpc_id description = "For test" name = "test-sg" tags = { Name = "test-sg" } } resource "aws_security_group_rule" "test" { security_group_id = aws_security_group.test_sg.id for_each = local.test_sg type = each.value[0] from_port = each.value[1] to_port = each.value[2] protocol = each.value[3] source_security_group_id = each.value[4] cidr_blocks = each.value[5] description = each.value[6] }
これらを terraform plan
し、問題なければ terraform apply
すれば完了です。
2. 起動テンプレートを構築する[Workloadアカウント作業]
起動テンプレートは以下 launch-template.tfに記述します。
## AMI data "aws_ami" "sample" { owners = ["111122223333"] most_recent = true filter { name = "name" values = ["sample*"] } } ## Launch template resource "aws_launch_template" "web" { name_prefix = "sample-lt-" image_id = data.aws_ami.sample.id instance_type = "t3.small" ebs_optimized = true block_device_mappings { device_name = "/dev/xvda" ebs { volume_size = "50" volume_type = "gp3" delete_on_termination = true } } update_default_version = true vpc_security_group_ids = [aws_security_group.test_sg.id] } lifecycle { create_before_destroy = true ignore_changes = [ default_version, latest_version, block_device_mappings, ] } } ## Security Groups locals { test_sg = { ## [ type, from_port, to_port, protocol, sg-id, cidr_blocks, description ] "rule_3" = ["egress", 0, 0, "-1", null, ["0.0.0.0/0"], "Allow any outbound traffic"] } } resource "aws_security_group" "test_sg" { vpc_id = module.vpc.vpc_id description = "For test" name = "test-sg" tags = { Name = "test-sg" } } resource "aws_security_group_rule" "test" { security_group_id = aws_security_group.test_sg.id for_each = local.test_sg type = each.value[0] from_port = each.value[1] to_port = each.value[2] protocol = each.value[3] source_security_group_id = each.value[4] cidr_blocks = each.value[5] description = each.value[6] } output "ami_id" { value = data.aws_ami.sample.id } output "ami_name" { value = data.aws_ami.sample.name }
補足
- AMIのデータソースが肝となりますが、4行目にて、Image BuilderによるAMI配布元AWSアカウントで絞ります。
- 5行目の、
most_recent=true
によって、複数の結果が返された場合は、最新のAMIを使用することができます。 - さらに、8、9行目のフィルターにてAMI名で絞ります。
- セキュリティグループは一旦先ほどと全く同じにしてます。
これらを terraform plan
し、問題なければ terraform apply
すれば完了です。
3. Image Builderにより新しいAMIを作成・配布する[Sharedアカウント作業]
マネジメントコンソールにて、作成したImage Builderイメージパイプラインを実行してイメージを作成します。
補足として、今回構成ではAMIのビルド自体はTerraformで行っていません。AMIのビルドは内容によって例えば30分など処理完了まで時間がかかり、かつ、コンソール画面の方が進捗状況も確認できるため、今回はコンソール作業としています。
実行すると、作成が開始されたイメージのステータスで進行していることが確認できます。
完了するとステータスは以下のように「使用可能」になりました。
(事前にいくつか作ってましたが)直近のイメージは sample|1.0.0/5
となっています。この場合、イメージレシピのバージョンが 1.0.0
、それによるビルドバージョンが 5
となります。
作成されたイメージのIDは ami-084c4299...
、 AMI名は sample-v5_2022_08_14...
であることが確認できます。
ディストリビューション設定にて、該当の組織IDが共有アクセスできる状態が確認できます。
4. 起動テンプレートを更新する[Workloadアカウント作業]
それでは次に、共有されたWorkloadアカウント側を見てみます。
現時点で前準備で、既に一度共有されたAMIの1つ前のビルドバージョン sample-v4...
を起動テンプレートに追加済みで、起動テンプレート済みであることが以下確認できます。
この状態で、 terraform plan
するとどうなるか見てみます。
terraform plan
を実行すると、以下の通り、AMIを手順3にて作成した新しいAMI ID ami-084c4299...
に置き換えようとする挙動を確認することができました。
% terraform plan ~~~中略~~~ Terraform will perform the following actions: # aws_launch_template.web will be updated in-place ~ resource "aws_launch_template" "web" { id = "lt-0db85135510292ead" ~ image_id = "ami-009eb3b9fa9967c9d" -> "ami-084c429949f8663b0" name = "sample-lt-20220810084742373400000001" tags = {} # (11 unchanged attributes hidden) # (2 unchanged blocks hidden) } Plan: 0 to add, 1 to change, 0 to destroy. Changes to Outputs: ~ ami_id = "ami-009eb3b9fa9967c9d" -> "ami-084c429949f8663b0" ~ ami_name = "sample_v4_2022-08-10T09-01-50.490Z" -> "sample_v5_2022-08-24T04-13-00.262Z"
これで terraform apply
すれば、起動テンプレートのAMIが最新のものに置き換えることできます。
これによって、Workloadsアカウント側のコードを変更せずに、新しいAMIを検知し差し替えることが可能であることが確認できました。
検証は以上です。
終わりに
EC2 Image BuilderをSharedアカウントにてビルド・配布し、配布されたWorkloadアカウントにて terraform plan/apply
により起動テンプレートを自動更新することができました。マルチアカウント環境のAMI管理の効率化には、EC2 Image Builderが役立ちそうです。一度お試しあれ。
それでは今日はこの辺で。ちゃだいん(@chazuke4649)でした。